@elnora-ai/cli 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +169 -72
  2. package/package.json +5 -5
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 ? "2.0.3" : process.env.npm_package_version ?? "0.0.0-dev";
28
+ VERSION = true ? "2.0.5" : 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";
@@ -185,7 +185,7 @@ function formatErrorForHuman(err) {
185
185
  }
186
186
  return lines.join("\n");
187
187
  }
188
- var ElnoraError, AuthError, NotFoundError, RateLimitError, ValidationError, ServerError, EXIT_CODES, SCRUB_KEY_VALUE_RE, SCRUB_LONG_TOKEN_RE;
188
+ var ElnoraError, AuthError, NotFoundError, RateLimitError, ValidationError, ServerError, NetworkError, EXIT_CODES, SCRUB_KEY_VALUE_RE, SCRUB_LONG_TOKEN_RE;
189
189
  var init_errors = __esm({
190
190
  "src/lib/errors.ts"() {
191
191
  "use strict";
@@ -241,6 +241,19 @@ var init_errors = __esm({
241
241
  this.name = "ServerError";
242
242
  }
243
243
  };
244
+ NetworkError = class extends ElnoraError {
245
+ host;
246
+ constructor(host, cause) {
247
+ const target = host ? ` reaching ${host}` : "";
248
+ const reason = cause ? ` (${cause})` : "";
249
+ super(`Network error${target}${reason}`, {
250
+ code: "NETWORK_ERROR",
251
+ suggestion: host ? `Run 'elnora doctor' to diagnose connectivity, then confirm ${host} is reachable from this machine.` : "Run 'elnora doctor' to diagnose connectivity."
252
+ });
253
+ this.name = "NetworkError";
254
+ this.host = host;
255
+ }
256
+ };
244
257
  EXIT_CODES = /* @__PURE__ */ new Map([
245
258
  [ValidationError, 2],
246
259
  [AuthError, 3],
@@ -624,16 +637,17 @@ var init_client = __esm({
624
637
  });
625
638
  } catch (err) {
626
639
  if (err instanceof ElnoraError) throw err;
640
+ let host;
641
+ try {
642
+ host = new URL(url).hostname;
643
+ } catch {
644
+ host = void 0;
645
+ }
627
646
  if (err instanceof DOMException && err.name === "TimeoutError") {
628
- throw new ElnoraError("Request timed out", {
629
- code: "TIMEOUT",
630
- suggestion: "Check your internet connection and try again."
631
- });
647
+ throw new NetworkError(host, `timed out after ${this.timeout}ms`);
632
648
  }
633
- throw new ElnoraError(`Network error: ${scrub(String(err))}`, {
634
- code: "NETWORK_ERROR",
635
- suggestion: "Check your internet connection and try again."
636
- });
649
+ const causeCode = err instanceof Error && err.cause && typeof err.cause === "object" && "code" in err.cause ? String(err.cause.code) : err instanceof Error ? err.message : String(err);
650
+ throw new NetworkError(host, scrub(causeCode));
637
651
  }
638
652
  }
639
653
  throw new RateLimitError("Rate limit exceeded after retries");
@@ -1337,6 +1351,30 @@ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as rea
1337
1351
  import { homedir as homedir3 } from "node:os";
1338
1352
  import { join as join3 } from "node:path";
1339
1353
  import pc2 from "picocolors";
1354
+
1355
+ // src/lib/install-method.ts
1356
+ function detectInstallMethod() {
1357
+ const execPath = process.argv[1] ?? "";
1358
+ if (execPath.includes("node_modules")) {
1359
+ return "npm";
1360
+ }
1361
+ if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/")) {
1362
+ return "homebrew";
1363
+ }
1364
+ return "binary";
1365
+ }
1366
+ function getUpdateInstruction(method) {
1367
+ switch (method) {
1368
+ case "npm":
1369
+ return "npm install -g @elnora-ai/cli@latest";
1370
+ case "homebrew":
1371
+ return "brew upgrade elnora";
1372
+ case "binary":
1373
+ return "elnora update --install";
1374
+ }
1375
+ }
1376
+
1377
+ // src/lib/update-check.ts
1340
1378
  var NPM_REGISTRY_URL = "https://registry.npmjs.org/@elnora-ai/cli/latest";
1341
1379
  var CACHE_DIR = join3(homedir3(), ".elnora");
1342
1380
  var CACHE_FILE = join3(CACHE_DIR, ".update-check");
@@ -1366,13 +1404,15 @@ function writeCache(entry) {
1366
1404
  }
1367
1405
  function showUpdateNotice(latest) {
1368
1406
  if (!process.stderr.isTTY) return;
1407
+ const command = getUpdateInstruction(detectInstallMethod());
1369
1408
  const color2 = isColorEnabled();
1409
+ const head = `Update available: v${VERSION} \u2192 v${latest}`;
1370
1410
  const msg = color2 ? `
1371
- ${pc2.yellow(`Update available: v${VERSION} \u2192 v${latest}`)}
1372
- Run: elnora update
1411
+ ${pc2.yellow(head)}
1412
+ Run: ${command}
1373
1413
  ` : `
1374
- Update available: v${VERSION} \u2192 v${latest}
1375
- Run: elnora update
1414
+ ${head}
1415
+ Run: ${command}
1376
1416
  `;
1377
1417
  process.stderr.write(msg);
1378
1418
  }
@@ -1508,33 +1548,44 @@ function checkPathConfigured() {
1508
1548
  msg: "PATH configured standard install dirs not in PATH (OK if installed via npm/brew)"
1509
1549
  };
1510
1550
  }
1551
+ function readEnabledPlugins(file) {
1552
+ if (!existsSync4(file)) return null;
1553
+ try {
1554
+ const settings = JSON.parse(readFileSync4(file, "utf-8"));
1555
+ const raw = settings.enabledPlugins;
1556
+ const enabledPlugins = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
1557
+ return { enabledPlugins };
1558
+ } catch (e) {
1559
+ return { enabledPlugins: {}, unreadable: e instanceof Error ? e.message : "parse error" };
1560
+ }
1561
+ }
1511
1562
  function checkClaudePlugin() {
1512
1563
  if (!existsSync4(CLAUDE_DIR)) {
1513
1564
  return { status: "skip", msg: "Plugin enabled (Claude Code not installed)" };
1514
1565
  }
1515
- if (!existsSync4(CLAUDE_SETTINGS_FILE)) {
1566
+ const projectSettingsFile = join4(process.cwd(), ".claude", "settings.json");
1567
+ const user = readEnabledPlugins(CLAUDE_SETTINGS_FILE);
1568
+ const project = readEnabledPlugins(projectSettingsFile);
1569
+ if (!user && !project) {
1516
1570
  return { status: "fail", msg: "Plugin enabled settings.json not found \u2014 run 'elnora setup claude'" };
1517
1571
  }
1518
- let settings;
1519
- try {
1520
- settings = JSON.parse(readFileSync4(CLAUDE_SETTINGS_FILE, "utf-8"));
1521
- } catch (e) {
1522
- return {
1523
- status: "fail",
1524
- msg: `Plugin enabled settings.json unreadable (${e instanceof Error ? e.message : "parse error"})`
1525
- };
1572
+ if (user?.unreadable && !project?.enabledPlugins[PLUGIN_ID]) {
1573
+ return { status: "fail", msg: `Plugin enabled settings.json unreadable (${user.unreadable})` };
1526
1574
  }
1527
- const raw = settings.enabledPlugins;
1528
- const enabledPlugins = typeof raw === "object" && raw !== null && !Array.isArray(raw) ? raw : {};
1529
- const isEnabled = enabledPlugins[PLUGIN_ID] === true;
1530
- const legacyKeys = Object.keys(enabledPlugins).filter((k) => k.startsWith("elnora@") && k !== PLUGIN_ID);
1575
+ const userEnabled = user?.enabledPlugins[PLUGIN_ID] === true;
1576
+ const projectEnabled = project?.enabledPlugins[PLUGIN_ID] === true;
1577
+ const isEnabled = userEnabled || projectEnabled;
1578
+ const scope = projectEnabled ? userEnabled ? "user + project" : "project" : "user";
1579
+ const legacyKeys = [
1580
+ .../* @__PURE__ */ new Set([...Object.keys(user?.enabledPlugins ?? {}), ...Object.keys(project?.enabledPlugins ?? {})])
1581
+ ].filter((k) => k.startsWith("elnora@") && k !== PLUGIN_ID);
1531
1582
  if (isEnabled && legacyKeys.length === 0) {
1532
- return { status: "pass", msg: `Plugin enabled ${PLUGIN_ID}` };
1583
+ return { status: "pass", msg: `Plugin enabled ${PLUGIN_ID} (${scope} scope)` };
1533
1584
  }
1534
1585
  if (isEnabled && legacyKeys.length > 0) {
1535
1586
  return {
1536
1587
  status: "warn",
1537
- msg: `Plugin enabled ${PLUGIN_ID} \u2014 but stale legacy entries also present: ${legacyKeys.join(", ")}`
1588
+ msg: `Plugin enabled ${PLUGIN_ID} (${scope} scope) \u2014 but stale legacy entries also present: ${legacyKeys.join(", ")}`
1538
1589
  };
1539
1590
  }
1540
1591
  if (legacyKeys.length > 0) {
@@ -1545,15 +1596,46 @@ function checkClaudePlugin() {
1545
1596
  }
1546
1597
  return { status: "fail", msg: "Plugin enabled not found \u2014 run 'elnora setup claude'" };
1547
1598
  }
1599
+ function isMarketplaceRegistered() {
1600
+ for (const file of [CLAUDE_SETTINGS_FILE, join4(process.cwd(), ".claude", "settings.json")]) {
1601
+ if (!existsSync4(file)) continue;
1602
+ try {
1603
+ const settings = JSON.parse(readFileSync4(file, "utf-8"));
1604
+ const enabled = settings.enabledPlugins;
1605
+ if (typeof enabled === "object" && enabled !== null && enabled[PLUGIN_ID] === true) {
1606
+ return true;
1607
+ }
1608
+ const extra = settings.extraKnownMarketplaces;
1609
+ if (extra != null && JSON.stringify(extra).includes(MARKETPLACE_NAME)) return true;
1610
+ } catch {
1611
+ }
1612
+ }
1613
+ const knownMarketplaces = join4(CLAUDE_DIR, "plugins", "known_marketplaces.json");
1614
+ if (existsSync4(knownMarketplaces)) {
1615
+ try {
1616
+ if (JSON.stringify(JSON.parse(readFileSync4(knownMarketplaces, "utf-8"))).includes(MARKETPLACE_NAME)) {
1617
+ return true;
1618
+ }
1619
+ } catch {
1620
+ }
1621
+ }
1622
+ return false;
1623
+ }
1548
1624
  function checkSkillsInstalled() {
1549
1625
  if (!existsSync4(CLAUDE_DIR)) {
1550
1626
  return { status: "skip", msg: "Skills installed (Claude Code not installed)" };
1551
1627
  }
1552
1628
  const skillsDir = join4(CLAUDE_DIR, "plugins", "marketplaces", MARKETPLACE_NAME, "elnora", "skills");
1553
1629
  if (!existsSync4(skillsDir)) {
1630
+ if (isMarketplaceRegistered()) {
1631
+ return {
1632
+ status: "warn",
1633
+ msg: "Skills installed registered but not cloned yet \u2014 open this folder in Claude Code once to clone"
1634
+ };
1635
+ }
1554
1636
  return {
1555
1637
  status: "warn",
1556
- msg: "Skills installed marketplace not cloned yet \u2014 restart Claude Code to trigger clone"
1638
+ msg: "Skills installed marketplace not registered \u2014 run 'elnora setup claude'"
1557
1639
  };
1558
1640
  }
1559
1641
  const found = EXPECTED_SKILLS.filter((s) => existsSync4(join4(skillsDir, s, "SKILL.md")));
@@ -1642,16 +1724,25 @@ function addDoctorCommand(program2) {
1642
1724
  Elnora CLI v${VERSION}
1643
1725
  `);
1644
1726
  let passed = 0;
1727
+ let skipped = 0;
1645
1728
  let failed = 0;
1646
1729
  let warnings = 0;
1647
- let total = 0;
1648
1730
  const tally = (r) => {
1649
- total++;
1650
- if (r.status === "pass" || r.status === "skip") passed++;
1651
- else if (r.status === "warn") {
1652
- passed++;
1653
- warnings++;
1654
- } else failed++;
1731
+ switch (r.status) {
1732
+ case "pass":
1733
+ passed++;
1734
+ break;
1735
+ case "warn":
1736
+ passed++;
1737
+ warnings++;
1738
+ break;
1739
+ case "skip":
1740
+ skipped++;
1741
+ break;
1742
+ case "fail":
1743
+ failed++;
1744
+ break;
1745
+ }
1655
1746
  };
1656
1747
  console.error(section("CLI"));
1657
1748
  const cliChecks = [
@@ -1685,14 +1776,15 @@ Elnora CLI v${VERSION}
1685
1776
  tally(r);
1686
1777
  }
1687
1778
  console.error("");
1688
- if (failed === 0 && warnings === 0) {
1779
+ if (failed === 0 && warnings === 0 && skipped === 0) {
1689
1780
  console.error("All checks passed.");
1690
- } else if (failed === 0) {
1691
- console.error(`${passed}/${total} checks passed (${warnings} warning${warnings === 1 ? "" : "s"}).`);
1692
1781
  } else {
1693
- const warnSuffix = warnings > 0 ? ` (${warnings} warning${warnings === 1 ? "" : "s"})` : "";
1694
- console.error(`${passed}/${total} checks passed \u2014 ${failed} failed${warnSuffix}.`);
1695
- if (!existsSync4(PROFILES_FILE)) console.error("\nRun: elnora auth login");
1782
+ const parts = [`${passed} passed`];
1783
+ if (skipped > 0) parts.push(`${skipped} skipped`);
1784
+ if (warnings > 0) parts.push(`${warnings} warning${warnings === 1 ? "" : "s"}`);
1785
+ if (failed > 0) parts.push(`${failed} failed`);
1786
+ console.error(`${parts.join(", ")}.`);
1787
+ if (failed > 0 && !existsSync4(PROFILES_FILE)) console.error("\nRun: elnora auth login");
1696
1788
  }
1697
1789
  });
1698
1790
  }
@@ -2017,36 +2109,21 @@ function addSetupCommand(program2) {
2017
2109
  init_config();
2018
2110
  import { execFileSync } from "node:child_process";
2019
2111
  import { createHash } from "node:crypto";
2020
- import { chmodSync as chmodSync2, copyFileSync, createWriteStream, mkdirSync as mkdirSync4, readFileSync as readFileSync5, rmSync } from "node:fs";
2112
+ import {
2113
+ chmodSync as chmodSync2,
2114
+ copyFileSync,
2115
+ createWriteStream,
2116
+ mkdirSync as mkdirSync4,
2117
+ readdirSync,
2118
+ readFileSync as readFileSync5,
2119
+ renameSync,
2120
+ rmSync
2121
+ } from "node:fs";
2021
2122
  import { tmpdir } from "node:os";
2022
2123
  import { join as join7 } from "node:path";
2023
2124
  import { Readable } from "node:stream";
2024
2125
  import { pipeline } from "node:stream/promises";
2025
2126
  import pc4 from "picocolors";
2026
-
2027
- // src/lib/install-method.ts
2028
- function detectInstallMethod() {
2029
- const execPath = process.argv[1] ?? "";
2030
- if (execPath.includes("node_modules")) {
2031
- return "npm";
2032
- }
2033
- if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/")) {
2034
- return "homebrew";
2035
- }
2036
- return "binary";
2037
- }
2038
- function getUpdateInstruction(method) {
2039
- switch (method) {
2040
- case "npm":
2041
- return "npm install -g @elnora-ai/cli@latest";
2042
- case "homebrew":
2043
- return "brew upgrade elnora";
2044
- case "binary":
2045
- return "elnora update --install";
2046
- }
2047
- }
2048
-
2049
- // src/commands/update.ts
2050
2127
  var NPM_REGISTRY_URL2 = "https://registry.npmjs.org/@elnora-ai/cli/latest";
2051
2128
  var GITHUB_REPO = "Elnora-AI/elnora-cli";
2052
2129
  function getPlatformTarget() {
@@ -2074,6 +2151,25 @@ async function downloadToFile(url, dest) {
2074
2151
  const nodeStream = Readable.fromWeb(res.body);
2075
2152
  await pipeline(nodeStream, createWriteStream(dest));
2076
2153
  }
2154
+ function installWindowsBinary(srcPath, installDir) {
2155
+ mkdirSync4(installDir, { recursive: true });
2156
+ const dest = join7(installDir, "elnora.exe");
2157
+ try {
2158
+ renameSync(dest, `${dest}.old-${Date.now()}`);
2159
+ } catch (e) {
2160
+ if (e.code !== "ENOENT") throw e;
2161
+ }
2162
+ copyFileSync(srcPath, dest);
2163
+ for (const name of readdirSync(installDir)) {
2164
+ if (name.startsWith("elnora.exe.old-")) {
2165
+ try {
2166
+ rmSync(join7(installDir, name), { force: true });
2167
+ } catch {
2168
+ }
2169
+ }
2170
+ }
2171
+ return dest;
2172
+ }
2077
2173
  async function installBinaryUpdate(latest) {
2078
2174
  const color2 = isColorEnabled();
2079
2175
  const platformInfo = getPlatformTarget();
@@ -2121,8 +2217,7 @@ async function installBinaryUpdate(latest) {
2121
2217
  `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`
2122
2218
  ]);
2123
2219
  const installDir = join7(process.env.USERPROFILE ?? process.env.HOME ?? "", ".elnora", "bin");
2124
- mkdirSync4(installDir, { recursive: true });
2125
- copyFileSync(join7(extractDir, target), join7(installDir, "elnora.exe"));
2220
+ installWindowsBinary(join7(extractDir, target), installDir);
2126
2221
  } else {
2127
2222
  execFileSync("tar", ["-xzf", archivePath, "-C", tmp]);
2128
2223
  const installDir = process.env.ELNORA_INSTALL_DIR ?? join7(process.env.HOME ?? "", ".local", "bin");
@@ -2183,8 +2278,10 @@ function addUpdateCommand(program2) {
2183
2278
  }
2184
2279
  try {
2185
2280
  await installBinaryUpdate(latest);
2186
- } catch {
2187
- console.error("Update failed. Try manually:");
2281
+ } catch (err) {
2282
+ const reason = err instanceof Error ? err.message : String(err);
2283
+ console.error(`Update failed: ${reason}`);
2284
+ console.error("Try manually:");
2188
2285
  console.error(
2189
2286
  process.platform === "win32" ? " irm https://cli.elnora.ai/install.ps1 | iex" : " curl -fsSL https://cli.elnora.ai/install.sh | bash"
2190
2287
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elnora-ai/cli",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Elnora AI Platform CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,14 +44,14 @@
44
44
  "zod": "^4.4.3"
45
45
  },
46
46
  "devDependencies": {
47
- "@biomejs/biome": "^2.4.15",
47
+ "@biomejs/biome": "^2.4.16",
48
48
  "@types/express": "^5.0.6",
49
49
  "@types/node": "^25.9.1",
50
- "@yao-pkg/pkg": "^6.19.0",
50
+ "@yao-pkg/pkg": "^6.20.0",
51
51
  "esbuild": "^0.28.0",
52
- "tsx": "^4.22.3",
52
+ "tsx": "^4.22.4",
53
53
  "typescript": "^6.0.3",
54
- "vitest": "^4.1.6"
54
+ "vitest": "^4.1.8"
55
55
  },
56
56
  "scripts": {
57
57
  "postinstall": "node scripts/postinstall.mjs",